← 返回首页

vllm-ascend PR #18870 解读:FlashComm1 的"假开启"与 SP 归位

PR 地址:vllm-project/vllm-ascend · Pull Request #18870([BugFix][Model][v0.30.0] Restore GLM V1 FlashComm1 with PP)
GLM DSA V1 配 PP>1 时 FlashComm1 名开实关——本 PR 五条件重开 decoder SP、o_proj 交出归约权、在每个 PP 出口把布局聚回全量

1. PR 档案

项目内容
标题[BugFix][Model][v0.30.0] Restore GLM V1 FlashComm1 with PP
作者 / 状态aisong1988 · 开放中(作者自述:张量单测与 NPU 正确性/性能验证仍 pending)创建于 2026-10-15
目标分支releases/v0.30.0——与前一篇 PR #18671 同一条版本线
改动规模5 个文件,+176 / −28 行(2 个源码文件 + 3 个测试文件;diff 实测)
涉及模块vllm_ascend/patch/worker/patch_deepseek_v2.py、vllm_ascend/worker/model_runner_v1.py(另含 3 个测试文件)
关联 PR#18671(同链前作:PP=1 时 SP 布局贯穿 DSA-CP,本站已解读);跟随 #16819 的聚散方案
生效场景GLM DSA V1 + 全局 SP(FlashComm1)+ PP>1,dense 前缀 + 连续 MoE 层——我们环境 PP=1,不直接命中(第 6 章详说)
读前须知:四个名词 30 秒(更多缩写见文末第 8 章术语速查)
一句话总结 上游 vLLM 有一个 PP=1 guard:PP>1 时一刀切关掉 decoder 级 SP(因为上游 PP transport 期望全量激活)。于是 PP>1 的 GLM V1 出现名实不符:FlashComm1 状态上报为开启,decoder SP 实际被关,o_proj 还带着每层一次的白归约。本 PR 的思路是自己兑现上游的前提:五条件选路重开 stage 内 SP、o_proj 交出归约权、把"聚回全量"上移到每个 PP 出口、Runner 的 PP 中间张量口径改成全量——上游要什么布局,边界上就还给什么布局。
图 1 · PR 改动全景:2 个源码文件都在"壳"上,3 个测试文件组成回归网
PR #18870 改动全景:+176 / −28 · 5 个文件 源码 ×2(都在「壳」上:patch 管层外壳,Runner 管边界) patch/worker/patch_deepseek_v2.py 运行时补丁:模型 init 五条件重开 SP、o_proj 交归约权(链上主战场) forward:PP 出口聚全量上移到 transport 之前 worker/model_runner_v1.py PP 中间张量口径:新增 _pp_intermediate_token_count, 接收切片 / dummy 估显存 / 最大 buffer 三处统一走它 测试 ×3(回归网) test_patch_deepseek_v2.py 五条件选路矩阵(model_type×v2×pp×sp)+ PP 聚散 test_model_runner_v1.py 接收与 dummy 布局匹配模型 transport(新测试组) test_deepseek_dsa_cp.py 配置补 pipeline_parallel_size / use_sequence_parallel_moe 门卫(ops/mla.py)与后端九步(sfa_v1.py)一行没动——本 PR 全部改在「开关与边界」,不在注意力数学。 前作 #18671 拆的是 PP=1 时"聚了又切";本篇处理 PP>1 时"SP 被 guard 关掉"——同一套布局契约的 PP 维度。

2. 问题:guard 一刀切,FlashComm1 名开实关(改动前)

先理清四个角色怎么协作。TP=16 把一层切到 16 卡;SP 在 TP 组内再把 token 横切成连续段(每卡一段);PP 再把模型的 N 层按层切成分段(stage)接力——层是 PP 的切法,token 是 SP 的切法,两者正交。GLM DSA V1 开全局 SP(FlashComm1 模式)时,decode 侧的分工是:注意力投影不自带归约,由 decoder 收口统一归约——这样每层省一次集合通信,这正是 FlashComm1 的收益来源。

但上游 vLLM 立了一条 guard:PP>1 时关掉 decoder 级 SP。原因在上游自己的传输约定——它的 PP transport 期望拿到全量激活,如果 stage 内跑 SP、出口传出的是分片,上游的接法就对不上。guard 一刀切之后,PP>1 的 GLM V1 就出现了下面的名实不符(PP 切层与 SP 的关系见图 3):

改动前 · PP>1:左=业务主线(原样),右=框架线(guard 与归约) 用户输入:3500 字 prompt Token 化(词表 128k) ids [1, 3500] Token 嵌入 ≈ 1.05B 128k×8192 → [1, 3500, 8192] Block × N(每层 ≈ 1.1B):前 k 层 dense 前缀 → 后续连续 MoE 层 LayerNorm [1, 219, 8192] self_attn(MLA/DSA) latent KV · indexer top-2048 稀疏 Q/KV 低秩投影 latent 压缩(步 3) 稀疏注意力(top-2048) Q·Kᵀ/√d → softmax → ·V O 投影 残差连接 + LayerNorm [1, 219, 8192] MoE(路由 + 共享专家) 共享专家 每 token 必走 路由专家 top-k 选通 共享 + 路由加权合并 SP:各卡各算 219 分片 already_sequence_parallel 残差连接 + MTP 模块(投机解码,MTP=3) draft 层:与主模型层同构 LM Head(与嵌入共享权重) 8192×128k → logits [1, 128k] 生成下一个 Token decode 重复整条链,KV 从缓存取 框架 · SP 切片(TP=16) 每卡 [1, 219, 8192] Ⓑ PP=1 guard(上游) PP>1 关 decoder SP——transport 期望全量,索性一刀切 Ⓑ FlashComm1 上报=开启 名实不符:配置开着, decoder SP 实际被关 Ⓐ o_proj.reduce_results=True 每层投影后各归各的——白归约 本应由 decoder 统一归约 Ⓐ PP 出口:全量交下一 stage 现状靠 SP 关换来的——本 PR 要改这里 读法:左列=GLM DSA V1 的模型架构(dense 前缀+连续 MoE+MLA/DSA+MTP);右列=框架线的三处错位;虚线=主线在框架上的挂点。Ⓑ 是根源,Ⓐ 是代价。 问题不在十行示例的数学,在开关:FlashComm1 的收益一分没拿到,还每层多付一次归约。
图 2 · 改动前:Ⓑ guard 关 SP 是根源,Ⓐ 每层白归约是代价,FlashComm1 名开实关

ⒶⒷ 速查(后文正文与代码注释均用此编号)

编号名称位置与性质
Ⓐo_proj 自带归约每层投影后 reduce_results=True 各归各的,FlashComm1 语义下应由 decoder 统一归约——白做
ⒷPP=1 guard 关 SP上游在 PP>1 时关掉 decoder SP(transport 期望全量)——FlashComm1 名开实关的根源
PP 怎么切:按层切段(stage)接力;SP 在 stage 内切 token;PP 出口必须是全量 stage 0 层 0 至 层 k(dense 前缀+MoE) stage 1 层 k+1 至 层 2k … 更多 stage 层 = PP 的切法(本图示意 8 层 2 stage) token = SP 的切法(stage 内每卡一段) PP 出口约定:交给下一 stage 的必须是全量激活(上游 transport 的期望) stage 内部随便怎么切——前提是出口自己把布局还回去。这就是本 PR 的立足点 本 PR 的 scope:GLM DSA V1 + 全局 SP + dense 前缀 + 连续 MoE 层 交错 dense/MoE 配置保持原行为;#18671 的 DSA-CP 本地优化不受影响
图 3 · PP 按层切段、SP 按 token 切卡:出口全量是上游约定,stage 内开 SP 的前提是出口自己恢复布局

3. 核心思路:自己兑现上游的前提

guard 的担忧是对的——上游 transport 确实只认全量。本 PR 不跟 guard 对着干,而是把 guard 的前提自己兑现:stage 内部照开 SP(拿 FlashComm1 的收益),在每个 PP 出口把 hidden/residual 聚回全量再交给 transport;o_proj 的归约权从"每层各归各的"收归 decoder 统一收口;Runner 侧所有 PP 中间张量的口径同步改成全量。一句话:边界还上游一个全量,stage 内换回真正的 FlashComm1。改后的样子见图 4:

改动后 · 业务主线与图 2 完全一致;只动框架线:灰=消失的白归约、绿=新增、蓝=变化 用户输入:3500 字 prompt Token 化(词表 128k) ids [1, 3500] Token 嵌入 ≈ 1.05B 128k×8192 → [1, 3500, 8192] Block × N(每层 ≈ 1.1B):前 k 层 dense 前缀 → 后续连续 MoE 层 LayerNorm [1, 219, 8192] self_attn(MLA/DSA) latent KV · indexer top-2048 稀疏 Q/KV 低秩投影 latent 压缩(步 3) 稀疏注意力(top-2048) Q·Kᵀ/√d → softmax → ·V O 投影 残差连接 + LayerNorm [1, 219, 8192] MoE(路由 + 共享专家) 共享专家 每 token 必走 路由专家 top-k 选通 共享 + 路由加权合并 SP:各卡各算 219 分片 already_sequence_parallel 残差连接 + MTP 模块(投机解码,MTP=3) draft 层:与主模型层同构 LM Head(与嵌入共享权重) 8192×128k → logits [1, 128k] 生成下一个 Token decode 重复整条链,KV 从缓存取 框架 · SP 切片(不变) 每卡 [1, 219, 8192] 蓝 · 五条件全中:重开 decoder SP V1·glm_moe_dsa·moe_freq=1·PP>1·SP 仅本 stage 的 MoE+MLA 层动手(图 13) 灰 · Ⓐ 消失:o_proj 不再每层归约 reduce_results=False, 归约权收归 decoder 统一收口 绿 · 新增:PP 出口聚全量上移 cat→all_gather→截断→split, transport 之前每个出口都还全量 蓝 · Runner 口径改全量 _pp_intermediate_token_count 三处统一 读法:左列与图 2 逐格一致(=GLM DSA V1 架构),一个框都没动;右列三处互为因果——敢开 SP(蓝),是因为出口还全量(绿);Ⓐ 消失(灰),是 SP 开了归约权自然收归。 dense 前缀层不在选路范围(mlp 不是 MoE 的层不动);前作 #18671 的 DSA-CP 优化不受影响。
图 4 · 改动后:业务主线一个框都没动;五条件开 SP(蓝)、Ⓐ 白归约消失(灰)、PP 出口聚全量上移(绿)

4. 主线讲解:从架构全貌到这次 PR

先对齐地基。本章走两条线:线一·模型架构线——从模型架构看到 vllm-ascend 框架,看模型的每一件住哪、谁调它,4.1 先摆这块推理的朴素 PyTorch 写法、定位到框架加的壳(图 5 给出全局定位 + 4.2 目录地图·图 6);线二·PyTorch 实现线——从十行示例的 PyTorch 实现看到框架实现,看它被谁一步步算出来、每步为什么(4.3 流水线·图 7 → 4.4 全链时序·图 8 → 4.5 逐步收益·图 9);两条线在 4.6 的问题处交汇,4.7 讲这次 PR(图 10~14),4.8~4.10 三张表收拢,每张图配一段讲解。先说结论:attention 的数学一行没动;本 PR 改的全是"开关与边界"——层的 SP 开关、o_proj 的归约权、PP 出口的布局。

4.1 先看这块推理的原始写法:朴素 PyTorch

老规矩,先立对比基准——这块推理(一层 Transformer 的前向)用 PyTorch 朴素写出来长什么样。自己动手写模型推理就会这么写:单卡、没有任何并行概念(有删节)。十行示例管的是 attention 这一小块;这里把整层摆出来,MoE 一并看:

# 朴素 PyTorch 写法:一层 Transformer 的前向(单卡,无框架)
def layer_forward(hidden_states):
    h = input_layernorm(hidden_states)          # 进层先归一化
    h = self_attn(h)                            # 注意力:QKV 投影 → QKᵀ/softmax/·V → o_proj(十行示例就是这段)
    hidden_states = h + hidden_states           # 残差 1
    h = post_attention_layernorm(hidden_states) # MoE 前再归一化
    h = mlp(hidden_states=h)                    # 前馈;GLM 这层是 MoE:路由选专家 → 专家 FFN → 加权合并
    return h + hidden_states                    # 残差 2 → 交给下一层

# 模型级就是 for 循环把 N 层串起来,最后 norm + lm_head 出下一个 token

这次 PR 动的不是这段数学——数学一行没动。动的是 vLLM 框架给这段朴素代码加的壳:进了框架,这一层被接上三件并行零件——层的 SP 开关(Ⓑ)、o_proj 的归约接线(Ⓐ)、PP 出口的交接。裸 PyTorch 里根本没有这些东西,它们全是框架为多卡布局塞进来的;PP>1 时框架还一刀切关掉 SP,让 Ⓐ 每层白付一次归约,而 FlashComm1 配置位照样报开启——名实不符,问题全住在壳上(ⒶⒷ 编号见第 2 章)。

这段代码在全局的什么位置?把模型架构到框架的全貌画出来(图 5):

模型架构 → 框架流程:模型的每一件,在 vllm-ascend 里住哪、谁调它 模型架构(自上而下) 框架落点 · 调用者(文件:行号实测) Token 嵌入 词表 128k · hidden 8192 上游 runner 喂 ids [1,3500],嵌入成 [1,3500,8192]; pad 到 3504 在 Runner 做(model_runner_v1.py:2935) × N 层 DecoderLayer 每层≈1.1B,外壳串四件 层外壳 forward:input LN → self_attn → post LN → MoE (deepseek_v2.py:1315);★ 运行时补丁 patch/ 在这重开 SP、 o_proj 交出归约权(patch_deepseek_v2.py) └ self_attn(十行示例) MLA/DSA:latent KV+top-2048 门卫 ops/mla.py:151 放行 → 后端 sfa_v1.py:1503 九步流水线开算;GLM 与 DeepSeek 共用这条实现链 └ MoE(路由+共享) SwiGLU,≈0.8B/层 SP MoE:各卡各算自己 219 个 token 的分片,天然免通信 (mlp 收 already_sequence_parallel=True,不再整理布局) MTP 模块(投机解码) 环境 MTP=3 vllm_ascend/models/deepseek_mtp.py:投机解码模块, 本 PR 不动; 改的落点在层外壳与 PP 边界(见 ★) LM Head(与嵌入共享) logits [1,128k] Runner 收回输出送采样;decode 每出一个 token, 再从第 2 行整条链走一遍(KV 从页表缓存取) 读法:左列=模型自上而下的架构件;右列=它在框架里的落点与调用者;★=本 PR(18870)落点:运行时补丁 patch/ 重开 SP。链子:Runner→层外壳→门卫→后端九步。 下一步:先认目录分工(图 6),再从十行示例进流水线(图 7);完整调用时序见时序图(图 8),每步为什么这么实现见展开图(图 9)。
图 5 · 模型架构 → 框架流程:模型的每一件在 vllm-ascend 里的落点与调用者(★ = 本 PR 改动落点)

看懂这张图,再定位本 PR。架构件一个没变、链子一段没短——变的是"运行时补丁 patch/" 这一层的两个开关:层外壳的 SP 开关(use_sequence_parallel_moe)和 o_proj 的归约开关(reduce_results)。这就是为什么图 5 的 ★ 落在层外壳行,而不是任何注意力算子上。

4.2 先认目录:与 attention 相关的目录,一个一个认

与 attention 相关的目录:左 = 目录名,右 = 它管什么、为什么有它 vllm_ascend/worker/ 执行与喂料 ★ 接引擎的批 → pad 到 3504 → 调模型、收回输出 为什么有它:上游执行器绑死 NVIDIA,昇腾必须有自己的执行器 vllm_ascend/models/ 模型结构 层怎么搭、forward 怎么走 —— 十行示例的调用者 为什么有它:模型结构上游都有,昇腾只改个别层,继承上游小改 vllm_ascend/ops/ 算子门面 把昇腾算子包成 vLLM 认识的"层";attention 的门卫(mla.py)在这 为什么有它:调用约定不同,模型层只认门面、不认具体硬件 vllm_ascend/attention/ 注意力实现 self_attn 的后端:九步流水线(sfa_v1.py:1503)在这里真正开算 为什么有它:注意力后端可插拔(不同硬件/并行形态各一套),策略独立成模块 …/attention/context_parallel/ DSA-CP 特化(车间) ⑤ 校验 · ③ 取命中 KV · ⑥ 收口 —— 上下文并行的专属逻辑 为什么有它:上下文并行(KV 与计算再切多卡)的特化单独放一个子目录 patch/(运行时补丁)★ 本 PR 主战场之一:重开 SP、聚散搬移都在这;platform.py(平台注册)知道即可
图 6 · 目录介绍:★ = 本 PR 改动文件所在的目录(patch/ 与 worker/)

四个目录,一条链,外加一个补丁层。worker/ 管执行与喂料,models/ 管模型结构,ops/ 管算子门面,attention/ 管注意力实现,patch/ 管运行时补丁——本 PR 的两个源码文件恰好一个在 patch/、一个在 worker/:它不改结构、不改算子,改的是"结构与算子被怎么组装、边界按什么口径喂"。

4.3 实现线:attention 在 vllm-ascend 原本是怎么完成的

架构线走完,现在切到实现线:attention 数学(图 7 左列的十行,L1~L10)在 vllm-ascend 里被一步步完成——左列每行,中列都有落点:

十行 attention 示例 → vllm-ascend 怎么完成它:左列实线是十行的流向,虚线弧线指向它在昇腾里的落点 十行 attention(样例代码) vllm-ascend 的实现流水线 在哪个模块 · 门牌(最全) L1 Q 投影 q = x @ wq L2 K/V 投影 k=x@wk, v=x@wv L3 RoPE 只转 Q、K L4 KV Cache cat 追加历史 L5 GQA repeat 复用 KV 给 Q 头 L6 打分 q @ kᵀ/√dk L7 因果遮罩 遮住未来 token L8 softmax 归一化 L9 加权和 w @ v_full L10 输出投影 out @ wo 层外壳发起 self_attn 门卫放行 → 后端 步3 Q/KV 联合低秩投影(KV 段) 步4 K 侧 rope · 步6 Q 侧 rope 步4 写本卡缓存 · 步7 全量写回 无此步(latent KV 全头共享) 步8 稀疏注意力算子(QKᵀ) 步8 内:causal 遮罩处理 步8 内:softmax 归一化 步8 内:·V 加权求和 步9 收口(o_proj)→交回层外壳 attention/ 步3:1576 (Q 低秩段)· 步6:1614 Q 上投影 矩阵乘原样,拆低秩两段 attention/ 步3(KV 段) 步4 :1595 exec_kv 投影+rmsnorm+写cache 融成一个昇腾算子 attention/ :1595(K 侧) :1616(Q 侧) 同一旋转,没变 融进算子,数学一致 attention/ :1626 reshape_and_cache slot 页表逐 token 定位 代替整块 cat 追加 attention/ MLA·DSA 特性 latent KV 全头共享 目的相同(省 KV) 手段不同,无独立步骤 attention/ 步8 :1683 内 QKᵀ/√dk 原样 只在 top-2048 块上算 稀疏,不扫全量历史 attention/ 步8 算子内 causal 遮罩由算子处理 prefill 生效 decode 无需遮罩 attention/ 步8 算子内 softmax 原样 归一化没变 数值在算子内部完成 attention/ 步8 算子内 ·V 加权原样 输出 [219,全头] 等待收口 attention/ :1695 o_proj 原路:直接投影 本 PR:归约权转移 (reduce_results,见 4.7) 读法:左列实线 = 十行示例自上而下,一行一格、零省略;弯曲虚线 = 每一行在 vllm-ascend 里的落点;右列门牌最全(模块 / 文件:行号 / 为什么)。 此时还没有 PR 的事——这就是 vllm-ascend 平时跑这份十行示例的样子。
图 7 · 流水线(原本):十行示例在 vllm-ascend 里的完成过程(没有这次 PR 时的样子)

看懂这张图,记住三件事。

一、十行示例一步没少。左列每一行,中列都有对应——投影、RoPE、写缓存、稀疏注意力、收口,数学原样;唯一的例外是 L5(GQA repeat),MLA 系用 latent KV 全头共享达到同样目的,所以没有独立步骤。

二、实现分散在三个模块,但只有一条调用链。models/ 发起 → ops/ 放行 → attention/ 干活;右列就是每一站的地盘和门牌(文件:行号)。

三、本 PR 不碰这张图。九步流水线是"stage 内怎么算",本 PR 管的是"层外壳的开关"与"PP 边界的布局"——往下看时序图时请带着这个分工。

4.4 全链时序:谁调用谁,每一棒先自己干再交棒

图 7 看的是"数学落在哪"。这份工作是谁发起、又一层层交给谁的?把整条调用链按时间拉开——六个类六条泳道,每个函数先标"自己干了啥",再标"委托给谁":

vllm-ascend 跑十行 attention 示例:全链时序——每个函数先"自己:"干完,再"委托:"下一棒(文件:行号实测) 上游 vLLM 引擎 组批·调度 core.py:595 Worker 昇腾插件 NPUWorker worker.py:643→669 执行器·喂料 NPUModelRunner model_runner_v1.py :2035 主入口 模型层 models/ AscendDeepSeekMTP deepseek_mtp.py:23 层外壳上游 :1315 门卫 ops/mla.py AscendMultiHead LatentAttention :151/:163/:181 后端 attention/ AscendSFAImpl / AscendSFADSACPImpl sfa_v1.py:1503 sfa_cp.py:292 自己:组批——调度器把多条请求 拼成一个批;每条请求的 attention 各自算各自的(按请求边界遮罩,互不 串);批越大 NPU 一趟吃越满(core.py:595) 委托:execute_model(批)——整批交给 Worker 引擎此后只收 output,再组下一批 自己:不碰 attention 数学——监控统计; PP 按层切段部署时,段与段之间收发 hidden(下一段 attention 就吃这份输入) (worker.py:643→669) 委托:model_runner.execute_model(:669) ——喂料员接手,把批变成算子要的形状 自己:喂料——pad 3500→3504(16 的倍数, 昇腾矩阵单元才吃得满);再组注意力 元数据:每条请求的边界+KV slot 起点 ——attention 算子按它切请求,免造 整块大 mask 张量(:2935·build :3294) 委托:模型 forward——层外壳逐层发起, 每层一次 attention+MoE 自己:MTP 投影层(draft,与主层同构—— 这里也住着一个 attention)——加载期 rot 一次性把权重排成后端要的形状, 免得每次 forward 重排(deepseek_mtp.py:23) 自己(层外壳):LN→AG 聚全量(每卡 219 hidden 做 AG 拼成 3504——十行示例的 hidden_states 输入,就是聚完的它)→ self_attn→RS 散回→postLN→MoE(:1315) 委托:self_attn(positions,hidden)—— 十行示例,从这一行进入框架 自己:预建输出张量 output——十行 的 attn_output 落这;预分配免碎片 (mla.py:151) 自己:custom op 直通车(:163)—— 编译器到此止步,防图优化把后端 的融合算子拆散——护住省带宽的融合 自己:取层权重/KV 句柄/批元数据, 凑齐后端开算要的三样再递进去 (mla_forward 函数体) 委托:impl.forward(层名,hidden,kv, 元数据,output)(:181)——进车间 impl 谁干活,加载期 resolve_sfa_impl 四 选一定死(:1303)——运行时每层直接调, 零选路开销;开 DSA-CP 选 AscendSFADSACPImpl(sfa_cp.py:292) 步准备:DSACPContext 立坐标——本卡管 哪段:local_start=rank×219;全量 3504=round_up(3500,16)(:1527/CP:188) 后面九步全按这套坐标切活:Q 按卡 切段(各 219),KV 不切(全量各存) 步3:Q/KV 联合低秩投影(:1576)——十行 的 q/k/v_proj 三个矩阵乘合成一枪; KV 压成低秩 latent——cache 按它记账 不按头数记,这就是省显存的源头 步4:exec_kv 融合算子(:1595)——十行 里 K/V 侧的投影+rmsnorm+rope+写 cache 融成一枪;中间结果不落显存(省带宽) 跨卡取命中 KV 拼全量(:1605)——算 QKᵀ 要全量历史 K,本卡只存了 1/16; 只取 indexer 命中的块,不全搬—— 通信量按命中块数走,不按历史长度走 步6:Q 投影+Q 侧 rope(:1614/:1616)—— 十行的 q_proj+rope(q);Q 不进 cache, 随算随转,不占显存 步7:写回全量 cache(_store_parallel_kv :1626)——本卡算出的 latent KV 每卡 各存一份,供别卡日后跨卡来取—— 代价 KV×16 份,拿显存换互通 选块:indexer 选 top-2048 块(:1670)—— 十行里没有这步(DSA 加的):长历史里 真正相关的只有少数块,先挑后算 步8:稀疏注意力算子(:1683)——十行的 QKᵀ/遮罩/softmax/·V 四步数学在这 一个算子里出齐;分数矩阵全程不落 显存——省带宽,这正是融合的意义 步9:收口 o_proj(:1695)——十行的 o_proj 原样;输出只剩本卡的 219 段, 交回门卫→层外壳,后面 RS 散回各卡 (原路 :1384) output 原路返回:后端→门卫→层外壳 →Runner→Worker→引擎,再进下一层 读法:"自己:"框=自己干的活——每框写清两件事:跟十行示例什么关系、省什么(显存/带宽/算力);"委托:"箭头=交给下一棒;右侧自转圈=九步流水线(同图7 中列)。 每层都走一遍右半边,本图只画一次;impl 在加载期定死。本 PR(18870)不动门卫与后端——它改层外壳的 SP 开关与 PP 边界(见 4.7)。
图 8 · 全链时序图:从引擎到后端,每个类"自己做什么 / 委托什么"——每框写明跟十行的关系与省什么(文件与行号本地实测)

读这张图抓三个节奏。

一、委托链只有一条,没有岔路。引擎组批 → Worker 接批 → Runner 喂料 → 层外壳发起 → 门卫放行 → 后端开算;每一棒只把活交给下一棒,谁也不越级。

二、"自己干"的都是壳上的杂活,"委托"出去的才是正餐。Worker 自己只做监控与流水线收发;Runner 自己只做 pad 和组元数据;把十行示例真正算出来的,是最右边后端的那串自转圈。

三、本 PR 动的是泳道之外的两样东西。层外壳进来的姿势(SP 开没开、o_proj 归不归约)和 PP 出口交出去的姿势(全量还是分片)——泳道内的每一棒都没变。

4.5 为什么这么实现:逐步收益

图 7 回答了"落在哪",图 8 回答了"谁调用谁",还剩最后一个问题:每一步为什么这么实现?各自省的是什么?把图 7 中列的流水线原样搬到最左列,向右逐层展开——怎么做(带门牌)→ 为什么这么设计 → 收益点:

每一步为什么这么实现:流水线(最左)→怎么做→为什么→收益点,逐层展开 昇腾实现流水线 (=图7 中列) 怎么做 · 门牌 (本地实测) 为什么这么设计 收益点(省什么) 层外壳 self_attn 门卫放行 → 后端 十行 L1 的调用者 层外壳串 LN→AG→attn →RS;门卫 :151 放行 层管布局,门卫管接入—— 模型代码不碰硬件细节 接法与算法分家 换后端只动门卫 模型层零改动 多卡逻辑收在壳上 步3 Q/KV 联合 低秩投影(KV 段) fused_qkv_a_proj(:1576) 一次矩阵乘同出 Q 路 1536 与 KV latent,后段再上投影 MLA 先压低秩 latent, 数学等价;KV 账本按 latent 记,不按全头记 KV Cache 不随头数 翻倍——省显存是大头; 三投影合一省访存 步4 K 侧 rope 步6 Q 侧 rope K 侧旋转融进 exec_kv (:1595);Q 侧 rope_single (:1616)就地转 K 转后才写 cache, Q 不进 cache 随算随转 ——顺序不能反 少一次中间张量读写; 旋转数学与十行的 L3 逐位一致 步4 写本卡缓存 步7 全量写回 slot 页表逐 token 定位写 (:1626);DSA-CP 跨卡拼 全量、每卡各存一份(:1605) 页表代替整块 cat; 每卡全存是跨卡取块 命中块的前提 无大拷贝、无碎片; 代价 KV×16 份—— 拿显存换正确性 无此步(GQA) latent 全头共享 GQA 的 repeat 没有对应物: latent KV 生来全头共用 (:1595 段内) GQA 用 repeat 凑复用; MLA 把复用直接做进 结构里 显存不随 Q 头数翻倍; 省掉 repeat 的拷贝 与算力(对比十行的 L5) 步8 稀疏注意力 算子(QKᵀ) indexer 先选 top-2048 块 (:1670);算子只在选中块 上 QKᵀ/√dk(:1683) 长历史里相关的只有 少数块;索引器先挑, 注意力再算 算力从扫全量历史降到 2048 块;序列越长 省得越多 步8 内: causal 遮罩处理 遮罩由算子内部按 prefill/decode 自动处理 (:1683 算子内) 遮罩是算子固定语义, 不必在外面造 mask 张量 省 mask 张量的显存 与生成耗时;prefill 遮未来,decode 免遮 步8 内: softmax 归一化 softmax 在算子内部完成, 与 QKᵀ 连续执行 (:1683 算子内) 三步数学融为一个 算子,中间结果不落地 分数矩阵不写显存—— 省带宽,这正是融合 的意义 步8 内: ·V 加权求和 ·V 加权同算子出结果 [219,全头],停在收口前 (:1683 算子内) 同一条算子流水线, 一次出全部分数结果 每卡只算自己 219 个 token 的输出,天然 按卡并行 步9 收口(o_proj) →交回层外壳 _finalize_o_proj(:1695; 原路 :1384)投影后交回; SP 时归约另有安排(18870) 输出投影保持原样; 通信只在 SP 需要 时才加 不开 SP 零额外通信; 开 SP 归约只做一次 (o_proj 两路图) 读法:最左列=图7 中列的流水线原样搬来;向右三列逐层展开——怎么做(带门牌)→为什么这么设计→收益点。口径:3504 全量/219 每卡/16 卡/top-2048 块。 本图讲"原本为什么这么实现"。本 PR(18870)九步一行没动——改的是层外壳 SP 开关与 PP 出口布局(见 4.7)。
图 9 · 昇腾实现展开图:最左列 = 图 7 中列流水线;向右逐层展开「怎么做 → 为什么 → 收益点」

逐行看收益,"省"分三种,省的东西不一样。

省显存的:低秩 latent 让 KV Cache 不随头数翻倍;slot 页表定位省掉整块 cat 的大拷贝。

省带宽的:融合算子让中间结果不落显存——投影+rmsnorm+rope+写缓存一个算子,QKᵀ→softmax→·V 一个算子。

省算力的:稀疏注意力只算 indexer 选中的 top-2048 块,不扫全量历史;序列越长,省得越多。

而本 PR 要省的,是这张图之外的一次通信:FlashComm1 语义下 o_proj 本可以不归约(每层省一次集合通信)——PP>1 时被 guard 一刀切关掉。九步数学没变,变的是开关。

4.6 问题:PP>1 时,SP 被 guard 关掉了

从引擎点到这段代码:模块 → 类 → 方法(改前 · 补丁链) 引擎 模块(定位) 类 方法 · 在这 上游 vLLM · 引擎 照常组批调执行 patch/ · 运行时补丁 patch_deepseek_v2.py DeepseekV2Model _patched_forward :293 层外壳 forward Ⓐ o_proj 自带归约 改前状态:层 SP 被 Ⓑ guard 关掉(use_sequence_parallel_moe=False)·o_proj.reduce_results=True 每层白归约·FlashComm1 名开实关
图 10 · 改前户口:模块 → 类 → 方法(patch_deepseek_v2.py 的 _patched_forward,层 SP 关、Ⓐ 白归约)

两条线在这里交汇:架构线上是 Ⓑ 开关问题,落到实现线上,它打在层外壳与 PP 边界——十行示例本身无恙。把问题钉在代码上。上游的 guard 长在层外壳的组装处:PP>1 时 use_sequence_parallel_moe 不会被置真,decoder SP 整段关闭;而 o_proj 是 RowParallelLinear,reduce_results=True 意味着每层投影后各自做一次归约——这在与 FlashComm1 语义(归约收归 decoder 统一做)组合时就是纯白做:

# 改前状态(上游 guard 生效时,有删节)
layer.use_sequence_parallel_moe = False        # Ⓑ guard:PP>1 时 decoder SP 被关
layer.self_attn.o_proj.reduce_results = True   # Ⓐ 每层投影后各归各的——白付一次集合通信
# 而配置侧 FlashComm1 仍是"开启"——名实不符就从这来

更隐蔽的是这份"名实不符"的迷惑性:看配置、看日志,FlashComm1 都像是在跑;只有对照层外壳才知道 SP 没开、归约白付。问题不在十行数学,在开关与它背后的前提。

4.7 这次 PR:五条件重开 SP,边界还上游一个全量

从引擎点到这段代码:模块 → 类 → 方法(改后 · PR 新补丁) 引擎 模块(定位) 类 方法 · 在这 上游 vLLM · 引擎 照常调到这一层 patch/ · 运行时补丁 init 补丁 :326 五条件 DeepseekV2Model 本 stage 的 MoE+MLA 层 层外壳 forward SP 开 · 归约权上交 改后:五条件全中才动手·层 use_sequence_parallel_moe=True·o_proj.reduce_results=False(归约交 decoder)·PP 出口聚全量上移 transport 前
图 11 · 改后户口:模型 init 补丁五条件选路(patch_deepseek_v2.py :326 起),层 SP 重开、o_proj 交出归约权

PR 的三处源码改动,一句话一个:

# PR 新增(patch_deepseek_v2.py :326,有删节):模型 init 补丁里的选路函数
def _enable_glm_pp_sequence_parallel(self, vllm_config) -> None:
    # 上游在 PP 下关 decoder SP,因为 transport 期望全量——
    # 本补丁的 forward 会在每个 PP 出口恢复全量,所以这里敢开
    if vllm_config.use_v2_model_runner:
        return                                     # 条件1:仅 V1 runner
    parallel_config = vllm_config.parallel_config
    if (vllm_config.model_config.hf_text_config.model_type != "glm_moe_dsa"
            or getattr(vllm_config.model_config.hf_text_config, "moe_layer_freq", 1) != 1
            or parallel_config.pipeline_parallel_size <= 1
            or not parallel_config.use_sequence_parallel_moe):
        return                                     # 条件2~5:GLM DSA / 层频=1 / PP>1 / SP 开
    for layer in islice(self.layers, self.start_layer, self.end_layer):
        if isinstance(layer.mlp, DeepseekV2MoE) and isinstance(layer.self_attn, DeepseekV2MLAAttention):
            layer.use_sequence_parallel_moe = True       # 重开 stage 内 SP(dense 层不动)
            layer.self_attn.o_proj.reduce_results = False  # 交出归约权:同一投影对象被 MLA 后端持有

选路是五条件与门(图 13):任一不满足就整段返回,行为与改前完全一致。动到的层必须"mlp 是 MoE 且 self_attn 是 MLA"——dense 前缀层天然被跳过。

这次 PR 改了什么:左列实线是原本的流向,虚线弧线指向 PR 的做法,右列解释最全 vllm-ascend 原本(PP>1) 这次 PR 的代码 / 做法 解释(最全) Ⓑ guard:PP>1 关 SP FlashComm1 名开实关 Ⓐ o_proj 自带归约 每层各归各的(白做) SP 开后:PP 出口是分片 transport 期望全量(矛盾) Runner:中间张量按分片 接收/估显存/最大buffer dense 前缀层 (mlp 非 MoE) 闸门:条件不满足 (行为=改前) init 补丁五条件与门 全中才对本 stage MoE+MLA 层动手 o_proj 交出归约权 reduce_results=False 归约收归 decoder 收口 聚全量上移 transport 前 cat→all_gather→截断→split 每个 PP 出口都还全量 口径统一走新 helper _pp_intermediate_token_count: GLM=全量;其它SP=ceil(n/tp) dense 层不动 isinstance 双查: mlp 是 MoE 且 attn 是 MLA 闸门:任一条件不满足 整段 return,行为=改前 guard 的担忧是对的 transport 确实只认全量; 错在一刀切,不是错在约束 (patch_deepseek_v2.py :326) 这就是 FlashComm1 的 本意:投影不归约, decoder 统一收口 每层省一次集合通信 敢开 SP 的底气: 自己兑现上游的前提; 同一段聚散 #16819 已用, 本 PR 把它搬到每个出口 三处硬编码替换: sync_and_slice 接收切片 / _dummy_run 估显存 / 最大 buffer(model_runner_v1.py) scope 边界: dense 前缀+连续 MoE 才动; 交错 dense/MoE 保持原行为 任何一级不满足: 回左列老路; 选路矩阵单测钉死 读法:左列实线=原本每一步;弯曲虚线=PR 在这一步动了什么;右列解释最全。黄=问题,蓝粗体=改动。 门卫与后端九步一行没动:本 PR 全部改在开关(patch 补丁)与边界(Runner 口径)。
图 12 · 流水线(这次 PR):左列原本(黄 = 问题),中列 PR 做法,右列解释;六行改动行行对应

这张图就是整个 PR。左列是原本(PP>1 的跑法),中列是 PR 的做法,逐行对着看:根源行(guard)由五条件与门取代;代价行(Ⓐ 白归约)由归约权上交消掉;矛盾行(SP 开则出口分片)由聚全量上移化解;口径行(Runner)由新 helper 统一;最后两行是 scope 与闸门——不满足条件就回老路。

生效条件:五条件与门,全中才动;任一不满足整段 return(行为=改前) 条件1 · V1 runner use_v2_model_runner == False 条件2 · 模型家族 model_type == "glm_moe_dsa" 条件3 · 层结构 moe_layer_freq == 1(连续 MoE) 条件4 · 流水线 pipeline_parallel_size > 1 条件5 · 全局开关 use_sequence_parallel_moe == True 与门 五条全中 全中 → 动手 层 SP 重开 + o_proj 交归约权(仅 MoE+MLA 层) 任一不中 → return 维持原行为, 与改前完全一致 选路矩阵单测 5 组参数化钉死:glm_moe_dsa×PP2×SP→开;PP1/SP关/V2 runner/deepseek_v3→全部不动。 注:层级的"mlp 是 MoE 且 attn 是 MLA"是第六道隐形门(isinstance 双查),dense 层在这里被跳过。
图 13 · 生效条件树:五条件与门 + 层级 isinstance 双查;任一不满足维持原行为

第二处源码改动是聚散搬移:同一段"聚回全量"的代码(cat → all_gather → 截断 → split),从"最后一个 rank 的 norm 之前"上移到"每个 PP 出口的 transport 之前"——改前 SP 关着这段代码形同虚设,改后 SP 开了它就是每个出口的必经站:

同一段聚全量代码:从"最后 rank 的 norm 前"上移到"每个 PP 出口的 transport 前" 改前 · 形同虚设: 层循环结束 SP 关,hidden=全量 聚全量块(在最后 rank norm 前) hidden 长度==positions 长度,不触发 norm → 返回 PP 出口交全量(靠 SP 关换来的) 改后 · 每个出口的必经站: 层循环结束 SP 开,hidden=分片 219 绿 · 聚全量块上移到 transport 前 cat→all_gather→截断→split,219→3504 PP transport → norm 交出去的永远符合上游期望 这就是敢跟 guard 讲条件的兑现书:上游要全量,出口就给全量;stage 内部才有资格开 SP。同一段聚散,#16819 已在用,本 PR 只搬家。 注意 hidden/residual 一起搬:cat 拼双份再 split,residual 还要 contiguous()——两个张量布局必须同步恢复。
图 14 · 聚散搬移:改前形同虚设(norm 前、不触发),改后是每个 PP 出口的必经站(transport 前)

4.8 对照总表:原本 vs 这次 PR,逐项对照

项原本(PP>1)这次 PR 后
decoder SPⒷ guard 关掉(use_sequence_parallel_moe=False)五条件全中时重开(仅本 stage 的 MoE+MLA 层)
o_proj 归约Ⓐ reduce_results=True,每层白归约reduce_results=False,归约收归 decoder 统一收口
PP 出口布局全量(靠 SP 关换来的)聚全量上移到 transport 前(cat→AG→截断→split),SP 开了也守约
Runner 中间张量SP 时按分片 ceil(n/tp)(三处硬编码)统一走 _pp_intermediate_token_count:GLM=全量,其它 SP 模型仍 ceil(n/tp)
dense 层不涉及isinstance 双查跳过,交错 dense/MoE 保持原行为
#18671 的 DSA-CP 优化——明确不动(PP=1 场景,与本 PR 的 PP>1 互不重叠)

4.9 PR 落点总表

文件挂在链上哪一环改动一句话
patch/worker/patch_deepseek_v2.py层外壳(模型 init + forward 补丁)五条件选路重开 SP + o_proj 交归约权 + 聚全量上移 transport 前
worker/model_runner_v1.pyPP 边界(中间张量口径)新增 _pp_intermediate_token_count,三处硬编码统一
tests/ut/patch/worker/test_patch_deepseek_v2.py选路回归五条件矩阵 5 组参数化 + PP 聚散传播用例补配置
tests/ut/worker/test_model_runner_v1.py口径回归新测试组:接收与 dummy 布局必须匹配模型 transport
tests/ut/models/test_deepseek_dsa_cp.py既有用例适配配置补 pipeline_parallel_size / use_sequence_parallel_moe 字段

4.10 测试(3 个文件,选路矩阵 + 口径匹配两道锁)

测试覆盖
test_glm_pp_sp_selection(patch 补丁)五条件矩阵参数化 5 组:glm_moe_dsa×V1×PP2×SP→开;PP=1 / SP 关 / V2 runner / deepseek_v3→全部不动
TestPPIntermediateLayout(Runner)glm_moe_dsa / deepseek_v3 × SP 开关 × 3/4 token:接收切片与 dummy buffer 长度必须等于模型 transport 的口径(GLM 恒全量,其它 SP 模型 ceil(n/tp))
既有用例配置补齐(dsa_cp)给 AscendDSACPModel 对比用例补 parallel_config 两个字段,适配新选路条件

5. 前后代码对照(带注释)

"改后"代码均摘自 PR 实际 diff(有删节,只保留主干);行号以 PR diff 为准。绿色为本 PR 新增逻辑;ⒶⒷ 编号对应图 2 / 图 4。

5.1 模型 init 补丁:五条件重开 SP、交归约权

改前 · init 补丁里没有选路(上游 guard 生效,有删节)
def _patched_deepseek_v2_model_init(self, *args, **kwargs):
    _original_deepseek_v2_model_init(self, *args, **kwargs)
    # (本 PR 之前)这里没有任何 PP/SP 选路——
    # PP>1 时层 SP 维持上游 guard 的关闭状态,Ⓑ 生效
    self._use_upstream_aux_relay = ...
    self.topk_indices_buffer = ...
改后 · 一行调用 + 选路函数(PR 新增,:306/:326)
def _patched_deepseek_v2_model_init(self, *args, **kwargs):
    _original_deepseek_v2_model_init(self, *args, **kwargs)
    _enable_glm_pp_sequence_parallel(self, kwargs["vllm_config"])  # ← 新增调用
    ...

def _enable_glm_pp_sequence_parallel(self, vllm_config) -> None:
    if vllm_config.use_v2_model_runner:
        return                                    # 条件1:仅 V1 runner
    parallel_config = vllm_config.parallel_config
    if (vllm_config.model_config.hf_text_config.model_type != "glm_moe_dsa"
            or getattr(vllm_config.model_config.hf_text_config, "moe_layer_freq", 1) != 1
            or parallel_config.pipeline_parallel_size <= 1
            or not parallel_config.use_sequence_parallel_moe):
        return                                    # 条件2~5
    for layer in islice(self.layers, self.start_layer, self.end_layer):
        if isinstance(layer.mlp, DeepseekV2MoE) and isinstance(layer.self_attn, DeepseekV2MLAAttention):
            layer.use_sequence_parallel_moe = True       # 重开 SP(dense 层被 isinstance 挡住)
            layer.self_attn.o_proj.reduce_results = False  # 归约权上交;同一投影对象被 Ascend MLA 后端持有

5.2 PP 出口:聚全量上移到 transport 之前(图 14)

改前 · 聚全量块在"最后 rank 的 norm 前"(SP 关时不触发,形同虚设)
    if not pp_group.is_last_rank:
        pp_output = IntermediateTensors(...)
        ...
        return pp_output
    # ↓ 下面这段只在最后一个 rank 执行得到
    if hidden_states.shape[0] != positions.shape[0]:
        combined = torch.cat([hidden_states, residual], dim=-1)
        combined = tensor_model_parallel_all_gather(combined, 0)
        ...
    hidden_states, _ = self.norm(hidden_states, residual)
改后 · 整块上移:transport 之前先还全量(PR 新增位置)
    # Restore full tokens before PP transport as well as the final norm.
    if hidden_states.shape[0] != positions.shape[0]:
        combined_states = torch.cat([hidden_states, residual], dim=-1)
        combined_states = tensor_model_parallel_all_gather(combined_states, 0)
        combined_states = combined_states[: positions.shape[0]]
        hidden_size = self.hidden_size
        hidden_states, residual = combined_states.split([hidden_size, hidden_size], dim=-1)
        residual = residual.contiguous()
    if not pp_group.is_last_rank:
        pp_output = IntermediateTensors({"hidden_states": hidden_states, "residual": residual})
        ...                                      # 出口交出去的必是全量
    hidden_states, _ = self.norm(hidden_states, residual)

5.3 Runner:PP 中间张量口径统一(图 15)

改后 · 新 helper + 三处替换(model_runner_v1.py :3279 起,有删节)
def _pp_intermediate_token_count(self, num_tokens: int) -> int:
    # GLM 的 decoder 在 stage 内用 SP,但补丁会在每个 PP 边界恢复全量
    if self.model_config.hf_text_config.model_type == "glm_moe_dsa" or not enable_sp():
        return num_tokens                        # GLM:全量
    tp = self.vllm_config.parallel_config.tensor_parallel_size
    return (num_tokens + tp - 1) // tp           # 其它 SP 模型:分片 ceil(n/tp)
# 三处硬编码替换:sync_and_slice_intermediate_tensors 的接收切片、
# _dummy_run 的显存预估与最大 buffer——口径从此只有一份
一个口径函数管三处:PP 中间张量到底按多少 token 算 glm_moe_dsa(或 SP 未开) 口径 = 全量 num_tokens——边界已还全量 其它 SP 模型 口径 = ceil(n/tp) 分片——沿原约定 调用点1 · 接收切片 sync_and_slice:copy_len 调用点2 · dummy 估显存 _dummy_run:防止多算一次 AG 调用点3 · 最大 buffer make_empty_intermediate_tensors 改前是三处各自硬编码(SP 时一律除以 tp);GLM 边界还全量后,统一走一个函数——口径只有一份,错也不会错三样。
图 15 · PP 中间张量口径:GLM=全量、其它 SP 模型=分片,三处调用点统一走 _pp_intermediate_token_count

5.4 归约权转移:一行改动的分量

layer.self_attn.o_proj.reduce_results = False
# 别看只是一行:o_proj 是 RowParallelLinear,reduce_results=True 时
# 每层投影后各自归约(Ⓐ);置 False 后归约权交给 decoder 统一收口——
# 注释原话:This is the same projection object held by the Ascend MLA backend.
# 即改的这个对象,就是门卫/后端链上真正干活的那个投影——改一处,全链生效

6. 与我们排查问题的关系

如实说:这个 PR 不直接打我们的场景 我们环境是双 A3、TP=16 DP=2、PP=1——guard 的条件(PP>1)在我们这根本不触发,所以 18671 的 DSA-CP 优化也不受本 PR 影响(它明确声明不动 #18671)。但它是同一套布局契约体系的 PP 维度补全:18671 解决"PP=1 时聚了又切",18870 解决"PP>1 时 SP 被 guard 关掉"。将来若多机切 PP(stage 接力),这个 PR 就是 FlashComm1 能开的前提。

方法论上值得记一笔:guard 的约束(transport 期望全量)是对的,错的是一刀切的实现。本 PR 的解法不是绕过约束,而是把约束的前提自己兑现——"边界还全量,stage 内才敢开 SP"。这个模式和 18671 的"布局契约"一脉相承:与其让开关一刀切,不如把布局责任钉在边界上。

PR 自述状态:ruff 通过;选路矩阵与口径匹配的回归用例已就位;张量单测与 NPU 正确性/性能验证仍 pending,此前一轮 CI 败在环境(pip 证书与 apt 签名),还没跑到测试。

7. 收益、风险与待验证

收益 PP>1 的 GLM DSA V1 部署真正拿到 FlashComm1:decoder SP 生效、Ⓐ 每层白归约消失、归约收归统一收口。无需新配置项——条件满足自动生效。
安全性设计 五条件与门 + 层级 isinstance 双查:模型/runner/PP/SP 任一不满足即整段返回,行为与改前一致;dense 前缀层不动;交错 dense/MoE 不动;#18671 的 DSA-CP 优化明确不变。
待验证(PR 自述) 张量单测未跑完;NPU 正确性与性能验证 pending;无性能数据(作者明说"No performance gain has been measured");前轮 CI 败在环境未到测试。当前开放中、以合入版为准。
对我们的行动建议

8. 术语速查

术语本 PR 语境下的含义
FlashComm1GLM V1 的 SP MoE 通信模式:decoder 开 SP、注意力投影不自带归约,归约交给统一收口
PP / stage流水线并行:按层把模型切段接力;本 PR 的开关卡在 PP>1
PP=1 guard上游在 PP>1 时关闭 decoder 级 SP 的一刀切(transport 期望全量激活)——Ⓑ 的根源
reduce_resultsRowParallelLinear 的归约开关:True=每层投影后各自归约(Ⓐ 白做),False=归约权上交
_pp_intermediate_token_count本 PR 新增的口径函数:GLM 恒全量、其它 SP 模型 ceil(n/tp),三处调用点统一
dense 前缀连续 MoE 层之前的稠密层——被 isinstance 双查跳过,永不参与选路
patch 机制vllm_ascend/patch/:运行时 monkeypatch 上游类(如 DeepseekV2Model.__init__/forward)——本 PR 的主战场

来源:github.com/vllm-project/vllm-ascend PR #18870(开放中,base: releases/v0.30.0,+176/−28,5 文件)· 本页为技术解读,行号以 PR diff 为准,数据均引自 PR 原文

华杉的技术博客 · 苏ICP备2026063680号-1
首页